Skip to content

fix: Strip timezone for PostgreSQL timestamps in DatabaseSessionService#4365

Open
filipecaixeta wants to merge 3 commits intogoogle:mainfrom
filipecaixeta:fix-postgresql-timestamp-timezone
Open

fix: Strip timezone for PostgreSQL timestamps in DatabaseSessionService#4365
filipecaixeta wants to merge 3 commits intogoogle:mainfrom
filipecaixeta:fix-postgresql-timestamp-timezone

Conversation

@filipecaixeta
Copy link

@filipecaixeta filipecaixeta commented Feb 3, 2026

Summary

  • Fixes DataError when using PostgreSQL with asyncpg for session storage
  • PostgreSQL's default TIMESTAMP type is WITHOUT TIME ZONE, which cannot accept timezone-aware datetime objects
  • The existing code handled this for SQLite but not PostgreSQL - this fix applies the same timezone stripping

Error

When creating a session with PostgreSQL + asyncpg, the following error occurs:

sqlalchemy.dialects.postgresql.asyncpg.Error: <class 'asyncpg.exceptions.DataError'>: 
invalid input for query argument $5: datetime.datetime(2026, 2, 3, 21, 32, 50, 353909, 
tzinfo=datetime.timezone.utc) (can't subtract offset-naive and offset-aware datetimes)

During the INSERT:

INSERT INTO sessions (app_name, user_id, id, state, create_time, update_time) 
VALUES ($1, $2, $3, $4, $5, $6)

Where $5 and $6 are timezone-aware datetimes being inserted into TIMESTAMP WITHOUT TIME ZONE columns.

Root Cause

Commit 1063fa5 changed from database-generated timestamps (func.now()) to explicit Python datetimes (datetime.now(timezone.utc)). The SQLite case was handled by stripping the timezone, but PostgreSQL was overlooked.

Test plan

  • Verified fix resolves the error when creating sessions with PostgreSQL + asyncpg
  • Existing unit tests pass

Fixes regression from #1733

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @filipecaixeta, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a critical DataError in PostgreSQL by implementing consistent timezone stripping for timestamps, mirroring the existing behavior for SQLite to accommodate PostgreSQL's TIMESTAMP WITHOUT TIME ZONE type. Concurrently, it enhances the A2A to GenAI part conversion mechanism by introducing and supporting a new thought metadata field across various part types, enabling richer contextual data transfer.

Highlights

  • PostgreSQL Timestamp Fix: Addressed a 'DataError: can't subtract offset-naive and offset-aware datetimes' that occurred when using PostgreSQL with asyncpg. This error stemmed from attempting to store timezone-aware datetime objects into PostgreSQL's default TIMESTAMP WITHOUT TIME ZONE column type.
  • Consistent Timezone Stripping: Extended the existing timezone stripping logic, previously applied only to SQLite, to now also include PostgreSQL. This ensures that datetime.now(timezone.utc) objects are converted to timezone-naive before being stored, aligning with the database's expected format.
  • New 'thought' Metadata Support: Introduced a new thought metadata field for various Part types (TextPart, FilePart, FunctionCall, FunctionResponse, CodeExecutionResult, ExecutableCode) during the conversion process between A2A and GenAI formats. This allows for the propagation of additional contextual information.
  • Expanded Unit Tests for 'thought' Metadata: Added comprehensive unit tests to validate the correct handling and round-trip conversion of the new thought metadata across all supported Part types, ensuring data integrity during format transformations.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/google/adk/a2a/converters/part_converter.py
    • Modified convert_a2a_part_to_genai_part to extract and assign a thought field from part.metadata to the genai_types.Part for TextPart, FilePart (URI and Bytes), FunctionCall, FunctionResponse, CodeExecutionResult, and ExecutableCode.
    • Modified convert_genai_part_to_a2a_part to include the thought field in the a2a_part.metadata when converting from GenAI to A2A for FilePart, DataPart (FunctionCall, FunctionResponse, CodeExecutionResult, ExecutableCode), and Blob types.
  • src/google/adk/sessions/database_session_service.py
    • Updated the create_session function to strip timezone information from datetime.now(timezone.utc) when the database dialect is 'postgresql', in addition to 'sqlite'.
  • tests/unittests/a2a/converters/test_part_converter.py
    • Added new unit tests to verify the conversion of A2A TextPart, FilePart (with URI and Bytes), FunctionCall, FunctionResponse, CodeExecutionResult, and ExecutableCode to GenAI Part, specifically checking for the correct handling of the thought metadata field (true, false, or none).
    • Added new round-trip tests to ensure that the thought metadata is correctly preserved when converting from GenAI Part to A2A Part and back to GenAI Part for TextPart, FilePart (with URI and Bytes), FunctionCall, FunctionResponse, CodeExecutionResult, and ExecutableCode.
    • Modified the UnsupportedPartType mock class to include a metadata = None attribute to prevent errors in updated conversion logic.
Activity
  • The author verified that the fix resolves the DataError when creating sessions with PostgreSQL and asyncpg.
  • Existing unit tests were confirmed to pass after the changes.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@adk-bot adk-bot added the services [Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc label Feb 3, 2026
@adk-bot
Copy link
Collaborator

adk-bot commented Feb 3, 2026

Response from ADK Triaging Agent

Hello @filipecaixeta, thank you for creating this PR!

This PR is a bug fix. Could you please associate the GitHub issue with this PR? If there is no existing issue, could you please create one?

In addition, could you please provide logs or a screenshot after the fix is applied?

This information will help reviewers to review your PR more efficiently. Thanks!

PostgreSQL's default TIMESTAMP type is WITHOUT TIME ZONE, which cannot
accept timezone-aware datetime objects from Python. This causes a
DataError when using asyncpg: "can't subtract offset-naive and
offset-aware datetimes".

The existing code already handled this for SQLite by stripping the
timezone, but PostgreSQL was not handled. This fix applies the same
treatment to PostgreSQL.

Fixes the regression introduced in commit 1063fa5 which changed from
database-generated timestamps (func.now()) to explicit Python datetimes.
@filipecaixeta filipecaixeta force-pushed the fix-postgresql-timestamp-timezone branch from 0022eb3 to e275b01 Compare February 3, 2026 22:22
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request primarily addresses a timezone-related issue with PostgreSQL timestamps, which is a necessary fix. The change in DatabaseSessionService is correct and I've added a minor suggestion for simplification. Additionally, a significant portion of this PR introduces support for thought metadata in part conversions. This seems to be a new feature and is well-tested. I've included a suggestion to refactor some duplicated code in the converter to improve maintainability. It would be beneficial to update the PR description to reflect the addition of the thought feature for better clarity and tracking.

I am having trouble creating individual review comments. Click here to see my feedback.

src/google/adk/a2a/converters/part_converter.py (229-295)

medium

There's significant code duplication in handling the conversion for function_call, function_response, code_execution_result, and executable_code. All these blocks follow the same pattern of creating a metadata dictionary and then a DataPart. This can be refactored to reduce repetition and improve maintainability by determining the part_data and metadata_type first, and then constructing the DataPart in a single common block.

  part_data = None
  metadata_type = None

  if part.function_call:
    part_data = part.function_call
    metadata_type = A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
  elif part.function_response:
    part_data = part.function_response
    metadata_type = A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
  elif part.code_execution_result:
    part_data = part.code_execution_result
    metadata_type = A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT
  elif part.executable_code:
    part_data = part.executable_code
    metadata_type = A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE

  if part_data:
    metadata = {
        _get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY): metadata_type
    }
    if part.thought is not None:
      metadata[_get_adk_metadata_key('thought')] = part.thought
    return a2a_types.Part(
        root=a2a_types.DataPart(
            data=part_data.model_dump(by_alias=True, exclude_none=True),
            metadata=metadata,
        )
    )

src/google/adk/sessions/database_session_service.py (299-302)

medium

The check for database dialects can be simplified to be more concise and maintainable. You can combine the checks into a single if statement using the in operator with a tuple of dialect names. This removes the need for intermediate variables and is easier to extend in the future.

      if self.db_engine.dialect.name in ("sqlite", "postgresql"):
        now = now.replace(tzinfo=None)

…ervice

Adds unit tests to verify that timezone-aware datetimes are correctly
converted to naive datetimes for SQLite and PostgreSQL dialects, which
require naive timestamps for their default TIMESTAMP column types.
@ryanaiagent ryanaiagent self-assigned this Feb 5, 2026
@ryanaiagent ryanaiagent added the request clarification [Status] The maintainer need clarification or more information from the author label Feb 5, 2026
@ryanaiagent
Copy link
Collaborator

Hi @filipecaixeta , Thank you for your contribution! We appreciate you taking the time to submit this pull request.

@ryanaiagent
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly addresses the timezone issue for PostgreSQL when creating a session by stripping the timezone information from the datetime object. The change is straightforward and the new tests verify the intended behavior for sqlite and postgresql dialects.

However, I've identified a similar issue in the append_event method that has not been addressed. On line 522, datetime.fromtimestamp(event.timestamp) is used for non-SQLite dialects, which creates a naive datetime based on the local timezone. This will lead to incorrect timestamps being stored for PostgreSQL and other databases. This should be changed to create a naive datetime from a UTC timestamp, similar to the logic for SQLite. While this is outside the direct changes in this PR, it's a critical bug that should be fixed to ensure consistent timestamp handling.

I have also left a few comments suggesting improvements: one to make the dialect check in create_session more concise, and two to improve the design of the new tests to make them more robust by testing behavior rather than duplicating implementation logic.



@pytest.mark.parametrize('dialect_name', ['sqlite', 'postgresql'])
def test_database_session_service_strips_timezone_for_dialect(dialect_name):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test currently simulates the logic within create_session rather than testing the method's behavior directly. This makes the test brittle, as it's decoupled from the actual implementation. It would be more robust to test the DatabaseSessionService.create_session method by mocking the database dependencies and asserting that a naive datetime is used for the specified dialects.

assert now.tzinfo is None


def test_database_session_service_preserves_timezone_for_other_dialects():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the test above, this test duplicates implementation logic. A more robust approach would be to call create_session with a mocked dialect (e.g., 'mysql') and verify that the timestamp passed to the storage layer remains timezone-aware, testing the actual behavior of the service.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@rusherman
Copy link

Hi, thanks for this fix — we hit the exact same issue with asyncpg + PostgreSQL.

One thing worth noting: while this PR fixes the create_session path, there's a related timezone semantic inconsistency in append_event (around line 516-521):

if is_sqlite:
    update_time = datetime.fromtimestamp(
        event.timestamp, timezone.utc
    ).replace(tzinfo=None)
else:
    update_time = datetime.fromtimestamp(event.timestamp)

The else branch (which covers PostgreSQL) uses datetime.fromtimestamp(event.timestamp) without a timezone argument, so it produces a naive datetime in the local timezone. The SQLite branch produces a naive datetime in UTC.

This doesn't cause an asyncpg error (since the result is already naive), but it means the same event.timestamp gets stored as different wall-clock values depending on the database backend — and on the server's local timezone. On a non-UTC server:

  • SQLite: stores 2026-02-06 06:48:16 (UTC)
  • PostgreSQL: stores 2026-02-06 14:48:16 (local, e.g. UTC+8)

The create_session path (which this PR fixes) had the same kind of issue — datetime.now(timezone.utc) produces UTC, so after .replace(tzinfo=None) the stored value is UTC-based. It might be worth making append_event consistent too:

# Consistent UTC for all backends
update_time = datetime.fromtimestamp(event.timestamp, timezone.utc)
if self.db_engine.dialect.name in ("sqlite", "postgresql"):
    update_time = update_time.replace(tzinfo=None)

@vietnamesekid
Copy link

Hi @filipecaixeta.

I have a related PR #4388 that takes a different approach fixing this at the schema level by using postgresql.TIMESTAMP(timezone=True) in PreciseTimestamp.load_dialect_impl()

I'd like to share some analysis on why I think stripping timezone (replace(tzinfo=None)) may not be sufficient for PostgreSQL, and why a schema-level fix might be more appropriate

The append_event inconsistency

As @rusherman also pointed out, there's a semantic inconsistency in append_event

if is_sqlite:
    update_time = datetime.fromtimestamp(
        event.timestamp, timezone.utc
    ).replace(tzinfo=None)
else:
    update_time = datetime.fromtimestamp(event.timestamp)  # uses LOCAL timezone

For the else branch (PostgreSQL/MySQL):

  • datetime.fromtimestamp(event.timestamp) return a naive datetime in the app server's local timezone
  • This gets stored as-is into a TIMESTAMP WITHOUT TIME ZONE column
  • If the app server is in PDT (UTC-7) and the DB server is in UTC, the stored value is off by 7 hours

This is exactly the bug described in #1848

Why replace(tzinfo=None) doesn't fully solve this?

Even if we add replace(tzinfo=None) for PostgreSQL in create_session, the append_event path still uses datetime.fromtimestamp(event.timestamp) which produces local-timezone naive datetimes. We'd need to fix that too, and at that point we're stripping timezone info from UTC datetimes just to store them in a timezone-unaware column fighting against the database rather than working with it.

Schema-level approach (PR #4388)

My PR instead makes PreciseTimestamp return postgresql.TIMESTAMP(timezone=True) for PostgreSQL, so the column type is TIMESTAMPTZ. This means:

  • create_session: datetime.now(timezone.utc) is stored correctly with timezone info preserved
  • append_event: Even naive datetimes are interpreted as the DB server's timezone (typically UTC), which is more predictable than depending on the app server's local timezone
  • Reading back: PostgreSQL always returns timezone-aware datetimes, so update_time.timestamp() computes correctly

The tradeoff is that existing PostgreSQL users need a one-time migration

ALTER TABLE sessions ALTER COLUMN create_time TYPE TIMESTAMP WITH TIME ZONE;
-- (and similar for other timestamp columns)

Ideally

The best fix would combine both approaches

  1. Schema-level: TIMESTAMP WITH TIME ZONE for PostgreSQL (my PR fix: use TIMESTAMP WITH TIME ZONE for PostgreSQL in PreciseTimestamp #4388)
  2. Code-level: Ensure append_event also uses UTC-aware datetimes consistently:
update_time = datetime.fromtimestamp(event.timestamp, timezone.utc)

This way all timestamps are explicitly UTC throughout the entire pipeline, regardless of the app server's timezone.
What do you think? Happy to coordinate on this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

request clarification [Status] The maintainer need clarification or more information from the author services [Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants